/***************************************************************************//**
 * @file
 * @brief This code simulates an ISA backchannel serial interface for a
 *   PC EZSP Host application.
 *   It creates a socket to listen for TCP connections so that scripts
 *   can treat a gateway application just like any other Insight adapter.
 *******************************************************************************
 * # License
 * <b>Copyright 2018 Silicon Laboratories Inc. www.silabs.com</b>
 *******************************************************************************
 *
 * The licensor of this software is Silicon Laboratories Inc. Your use of this
 * software is governed by the terms of Silicon Labs Master Software License
 * Agreement (MSLA) available at
 * www.silabs.com/about-us/legal/master-software-license-agreement. This
 * software is distributed to you in Source Code format and is governed by the
 * sections of the MSLA applicable to Source Code.
 *
 ******************************************************************************/

#include PLATFORM_HEADER //compiler/micro specifics, types

#include "stack/include/ember-types.h"
#include "stack/include/error.h"

#include "hal/hal.h"

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <errno.h>   // for errno
#include <unistd.h>  // for close()

#include "app/framework/plugin-host/gateway/gateway-support.h"

//------------------------------------------------------------------------------
// GLOBALS

const bool backchannelSupported = true;

// The serial port number (0 or 1) plus this offset gives the port that the
// socket server will listen on. By default (to match the InSight Adapter
// behavior):
//    * serial port 0 -> server port 4900
//    * serial port 1 -> server port 4901
int backchannelSerialPortOffset = EMBER_AF_PLUGIN_GATEWAY_TCP_PORT_OFFSET;

#define SERVER_PORT_OFFSET (backchannelSerialPortOffset)

#define INVALID_FD -1

static int socketFd[] = { INVALID_FD, INVALID_FD };
static int clientFd[] = { INVALID_FD, INVALID_FD };

struct sockaddr_in clientConnections[2];

static bool debugOn = false;
static const char debugString[] = "backchannel";

// These will change
static int LOCAL_STDIN = 0;
static int LOCAL_STDOUT = 1;
static int LOCAL_STDERR = 2;

#define REMOTE_STDIN  0
#define REMOTE_STDOUT 1
#define REMOTE_STDERR 2

#define MAX_STRING_LENGTH 250

// Turned on via command-line options
bool backchannelEnable = false;

static FILE *clientOut[] = { NULL, NULL };

//------------------------------------------------------------------------------
// Forward Declarations

static int willConnectionBlock(bool server, uint8_t port, bool read);

#define willClientConnectionBlock(port) \
  willConnectionBlock(false, (port), false)
#define willServerConnectionBlock(port) \
  willConnectionBlock(true, (port), true)

static bool getNewConnection(uint8_t port);
static void unixError(const char *format, ...);
static void debugPrint(const char *formatString, ...);
static void infoPrint(const char *formatString, ...);
static int myPrintf(int fd, const char *formatString, ...);
static int myVprintf(int fd, const char *formatString, va_list ap);

//------------------------------------------------------------------------------
// API

EmberStatus backchannelStartServer(uint8_t port)
{
    struct sockaddr_in serverAddress;
    int flags;

    if(!backchannelEnable)
    {
        return EMBER_INVALID_CALL;
    }

    if(port > 1)
    {
        return EMBER_SERIAL_INVALID_PORT;
    }

    if(socketFd[port] != -1)
    {
        return EMBER_INVALID_CALL;
    }

    socketFd[port] = socket(AF_INET, SOCK_STREAM, 0);
    if(socketFd[port] < 0)
    {
        unixError("Error: Could not open socket");
        return EMBER_ERR_FATAL;
    }
    flags = 1; // Enable SO_REUSEADDR to reduce bind() complaints
    (void) setsockopt(socketFd[port], SOL_SOCKET, SO_REUSEADDR, &flags,
                      sizeof(flags));

    bzero(&(clientConnections[port]), sizeof(struct sockaddr_in));

    bzero((char *) &serverAddress, sizeof(serverAddress));
    serverAddress.sin_family = AF_INET;
    serverAddress.sin_addr.s_addr = INADDR_ANY;
    serverAddress.sin_port = htons(SERVER_PORT_OFFSET + port);
    if(bind(socketFd[port],
            (struct sockaddr *) &serverAddress,
            sizeof(serverAddress)) < 0)
    {
        unixError("Error: Could not bind socket to %u", SERVER_PORT_OFFSET + port);
        return EMBER_ERR_FATAL;
    }
    if(0 > listen(socketFd[port], 0))
    {
        unixError("Error: Could not mark socket as listening");
        return EMBER_ERR_FATAL;
    }
    infoPrint("Listening for connections on port %u", SERVER_PORT_OFFSET + port);

    return EMBER_SUCCESS;
}


EmberStatus backchannelClientConnectionCleanup(uint8_t port)
{
    if(!backchannelEnable)
    {
        return EMBER_INVALID_CALL;
    }
    if(port > 1)
    {
        return EMBER_SERIAL_INVALID_PORT;
    }
    if(clientFd[port] != INVALID_FD)
    {
        myPrintf(REMOTE_STDOUT,
                 "Server closing client connection from %s:%u on port %u\n",
                 inet_ntoa(clientConnections[port].sin_addr),
                 ntohs(clientConnections[port].sin_port),
                 SERVER_PORT_OFFSET + port);
        infoPrint("Server closing client connection from %s:%u on port %u\n",
                  inet_ntoa(clientConnections[port].sin_addr),
                  ntohs(clientConnections[port].sin_port),
                  SERVER_PORT_OFFSET + port);
        close(clientFd[port]);
        clientFd[port] = INVALID_FD;
        bzero(&(clientConnections[port]), sizeof(struct sockaddr_in));
    }
    return EMBER_SUCCESS;
}

EmberStatus backchannelStopServer(uint8_t port)
{
    if(!backchannelEnable)
    {
        return EMBER_INVALID_CALL;
    }
    if(port > 1)
    {
        return EMBER_SERIAL_INVALID_PORT;
    }

    backchannelClientConnectionCleanup(port);

    if(socketFd[port] != INVALID_FD)
    {
        myPrintf(LOCAL_STDOUT, "Server closing socket connection %u\n",
                 SERVER_PORT_OFFSET + port);
        close(socketFd[port]);
        socketFd[port] = INVALID_FD;
    }
    return EMBER_SUCCESS;
}

// Retrieves a single byte from the client connection.  Returns the number
// of bytes read, or -1 on error.  If no client connection currently exists,
// then it will block until one is established.  If an error is returned then
// it means an attempt was made to establish one but it failed.
EmberStatus backchannelReceive(uint8_t port, char *data)
{
    if(!backchannelEnable)
    {
        return EMBER_INVALID_CALL;
    }
    if(port > 1 || clientFd[port] == INVALID_FD)
    {
        return EMBER_SERIAL_INVALID_PORT;
    }

    int result = recv(clientFd[port], data, 1, 0);
    if(result == 1)
    {
        return EMBER_SUCCESS;
    }
    else if(result == 0 || errno != EAGAIN)
    {
        backchannelCloseConnection(port);
        return EMBER_ERR_FATAL;
    }

    return EMBER_ERR_FATAL;
}

EmberStatus backchannelSend(uint8_t port, uint8_t *data, uint8_t length)
{
    if(!backchannelEnable)
    {
        return EMBER_INVALID_CALL;
    }
    if(port > 1)
    {
        return EMBER_SERIAL_INVALID_PORT;
    }
    if(INVALID_FD != clientFd[port]
            && !willClientConnectionBlock(port))
    {
        if(0 > send(clientFd[port], data, length, 0))
        {
            backchannelCloseConnection(port);
            return EMBER_ERR_FATAL;
        }
        return EMBER_SUCCESS;
    }
    return EMBER_INVALID_CALL;
}

// Checks on the state of the current backchannel connection.
// If one doesn't exist, it can wait for a new connection and return
// the result.
BackchannelState backchannelCheckConnection(uint8_t port,
        bool waitForConnection)
{
    if(!backchannelEnable)
    {
        return CONNECTION_ERROR;
    }
    else if(clientFd[port] >= 0)
    {
        return CONNECTION_EXISTS;
    }

    bool getConnection = false;

    if(waitForConnection)
    {
        infoPrint("Waiting for client connection on port %u",
                  port + SERVER_PORT_OFFSET);
        getConnection = true;
    }
    else if(!willServerConnectionBlock(port))
    {
        getConnection = true;
    }

    if(getConnection)
    {
        return (getNewConnection(port)
                ? NEW_CONNECTION
                : CONNECTION_ERROR);
    }
    return NO_CONNECTION;
}

EmberStatus backchannelServerPrintf(const char *formatString, ...)
{
    int stat;
    va_list ap = { 0 };
    va_start(ap, formatString);
    stat = myVprintf(LOCAL_STDOUT, formatString, ap);
    va_end(ap);
    return (stat == 0 ? EMBER_SUCCESS : EMBER_ERR_FATAL);
}

EmberStatus backchannelClientPrintf(uint8_t port, const char *formatString, ...)
{
    EmberStatus status;
    va_list ap = { 0 };
    va_start(ap, formatString);
    status = backchannelClientVprintf(port, formatString, ap);
    va_end(ap);
    return status;
}

EmberStatus backchannelClientVprintf(uint8_t port,
                                     const char *formatString,
                                     va_list ap)
{
    if(clientFd[port] != INVALID_FD)
    {
        int stat;
        vfprintf(clientOut[port], formatString, ap);
        stat = fflush(clientOut[port]);
        if(stat != 0)
        {
            backchannelCloseConnection(port);
            return EMBER_ERR_FATAL;
        }
        return EMBER_SUCCESS;
    }
    //  debugPrint("Serial port %d not valid!\n", port);
    return EMBER_SERIAL_INVALID_PORT;
}

// Re-map STDIN, STDOUT, and STDERR to the client connection.
// This allows the software to use normal read() and write() calls to
// to receive and send data to the remote client.
EmberStatus backchannelMapStandardInputOutputToRemoteConnection(int port)
{
    FILE **streams[] = { &stdin, &stdout, &stderr };
    uint8_t i;

    int connectionFd;
    if(!backchannelEnable)
    {
        return EMBER_INVALID_CALL;
    }
    if(port > 1)
    {
        return EMBER_SERIAL_INVALID_PORT;
    }
    connectionFd = clientFd[port];

    if(INVALID_FD == connectionFd)
    {
        return EMBER_ERR_FATAL;
    }

    int *localFds[] = { &LOCAL_STDIN, &LOCAL_STDOUT, &LOCAL_STDERR };

    for(i = 0; i < 3; i++)
    {
        // First re-map FD's 0,1,2 to a new local FD.
        *localFds[i] = dup(i);
        if(*localFds[i] < 0)
        {
            unixError("Could not dup() %d", i);
            close(connectionFd);
            clientFd[port] = INVALID_FD;
            return EMBER_ERR_FATAL;
        }

        // Now re-map the remotely connected FD to 0,1,2
        int newFd = dup2(connectionFd, i);
        if(newFd < 0)
        {
            unixError("Could not dup2() %d", connectionFd);
            close(connectionFd);
            clientFd[port] = INVALID_FD;
            return EMBER_ERR_FATAL;
        }
        // Redirect the FILE* globals (stdin, stdout, stderr) to the new socket
        // We don't perform an fclose() first because that will also close the FD,
        // which will prevent this fdopen() call from working.
        *(streams[i]) = fdopen(i, (i == 0 ? "r" : "a"));
        if(*(streams[i]) == NULL)
        {
            unixError("Could not fdopen() %d", i);
            close(connectionFd);
            clientFd[port] = INVALID_FD;
            return EMBER_ERR_FATAL;
        }
    }
    return EMBER_SUCCESS;
}

EmberStatus backchannelCloseConnection(uint8_t port)
{
    EmberStatus status = EMBER_SUCCESS;
    if(INVALID_FD == clientFd[port])
    {
        return EMBER_ERR_FATAL;
    }
    if(0 > close(clientFd[port]))
    {
        debugPrint("Error: Could not close socket: %s\n", strerror(errno));
        status = EMBER_ERR_FATAL;
    }
    else
    {
        infoPrint("Closed connection on port %d.", port);
    }
    clientFd[port] = -1;
    clientOut[port] = NULL;
    return status;
}

//------------------------------------------------------------------------------
// Internal Functions

// Returns 0 if a read/write will not block, 1 if it will, and -1 on error.
static int willConnectionBlock(bool server, uint8_t port, bool read)
{
    fd_set readSet;
    int fd = (server ? socketFd[port] : clientFd[port]);

    if(fd < 0)    // BugzId:12928
    {
        return -1;  // No connection - return error
    }

    FD_ZERO(&readSet);
    FD_SET(fd, &readSet);

    fd_set writeSet;
    FD_ZERO(&writeSet);
    FD_SET(fd, &writeSet);

    struct timeval noWaiting =
    {
        0,   // seconds
        0,   // milliseconds
    };

    int status = select(fd + 1,                  // highest FD + 1 (per select())
                        read ? &readSet : NULL,  // read FD set
                        read ? NULL : &writeSet, // write FD set
                        NULL,                    // exception FD set
                        &noWaiting);             // timeout

    if(status < 0)
    {
        // Theoretically select() can return prematurely due to a signal
        // and give an error of EINTR.  Because we do not wait,
        // we assume this is unlikely.
        unixError("select() Failed!");
        return -1;
    }

    // Invert the return code.  If select() saw the state of an FD change
    // before the timeout, it will return 1.  Otherwise it will return 0.
    // So if an FD is ready, we say it won't block (0).
    return (status ? 0 : 1);
}

// Accepts a new TCP connection.  Returns the state of the connection.
static bool getNewConnection(uint8_t port)
{
    socklen_t clientLength;
    struct sockaddr_in clientAddress;

    clientLength = sizeof(clientAddress);
    clientFd[port] = accept(socketFd[port],
                            (struct sockaddr *) &clientAddress,
                            &clientLength);
    debugPrint("New Client FD for port %d is %d", port, clientFd[port]);
    if(clientFd[port] >= 0)
    {
        // Code for making the port non-blocking
        //      flags = fcntl(clientFd[port], F_GETFL);
        //      if (fcntl(clientFd[port], F_SETFL, flags | O_NONBLOCK) < 0) {
        //        unixError("Error: Could not make connection non-blocking");
        //        backchannelCloseConnection(port);
        //        return NO_CONNECTION;
        //      }

        clientOut[port] = fdopen(clientFd[port], "a");
        if(clientOut[port] == NULL)
        {
            unixError("Error: Could not open stream to client connection.");
            backchannelCloseConnection(port);
            return false;
        }

        (void) memcpy(&(clientConnections[port]), &clientAddress, sizeof(struct sockaddr_in));
        infoPrint("New connection from client %s:%u on port %u",
                  inet_ntoa(clientConnections[port].sin_addr),
                  ntohs(clientConnections[port].sin_port),
                  SERVER_PORT_OFFSET + port);
        return true;
    }
    return false;
}

//------------------------------------------------------------------------------
// Internal Printing routines

// Print an error message plus the associated 'errno' string
static void unixError(const char *format, ...)
{
    va_list ap = { 0 };
    va_start(ap, format);
    myVprintf(LOCAL_STDERR, format, ap);
    va_end(ap);
    myPrintf(LOCAL_STDERR, ": %s\n", strerror(errno));
}

static void debugPrint(const char *formatString, ...)
{
    if(debugOn)
    {
        va_list ap = { 0 };
        myPrintf(LOCAL_STDERR, "[%s] ", debugString);
        va_start(ap, formatString);
        myVprintf(LOCAL_STDERR, formatString, ap);
        va_end(ap);
        myPrintf(LOCAL_STDERR, "\n");
    }
}

static void infoPrint(const char *formatString, ...)
{
    va_list ap = { 0 };
    va_start(ap, formatString);
    myVprintf(LOCAL_STDOUT, formatString, ap);
    va_end(ap);
    myPrintf(LOCAL_STDOUT, "\n");
}

// Because we mess around with FDs, we need our own printing routine
// for handling where messages are printed to.

static int myPrintf(int fd, const char *formatString, ...)
{
    va_list ap = { 0 };
    int returnCode;
    va_start(ap, formatString);
    returnCode = myVprintf(fd, formatString, ap);
    va_end(ap);
    return returnCode;
}

// Returns 0 on success, 1 on error
static int myVprintf(int fd, const char *formatString, va_list ap)
{
    int length;
    char string[MAX_STRING_LENGTH];
    length = vsnprintf(string, MAX_STRING_LENGTH - 1, formatString, ap);
    string[length] = '\0';
    return (length != write(fd, string, length));
}
